Skip to content

llm: add tabbed support to LLM Chat panel#7176

Open
psiinon wants to merge 2 commits into
zaproxy:mainfrom
psiinon:llm/tabs
Open

llm: add tabbed support to LLM Chat panel#7176
psiinon wants to merge 2 commits into
zaproxy:mainfrom
psiinon:llm/tabs

Conversation

@psiinon

@psiinon psiinon commented Mar 2, 2026

Copy link
Copy Markdown
Member

No description provided.

Signed-off-by: Simon Bennetts <psiinon@gmail.com>
@psiinon

psiinon commented Mar 2, 2026

Copy link
Copy Markdown
Member Author

Logo
Checkmarx One – Scan Summary & Details3c352489-aab3-464f-acf6-a42c8c6bcdb1


New Issues (2) Checkmarx found the following issues in this Pull Request
# Severity Issue Source File / Package Checkmarx Insight
1 MEDIUM Privacy_Violation /addOns/llm/src/main/java/org/zaproxy/addon/llm/ui/LlmChatTabPanel.java: 231
detailsMethod at line 231 of /addOns/llm/src/main/java/org/zaproxy/addon/llm/ui/LlmChatTabPanel.java sends user information outside the application. Th...
Attack Vector
2 MEDIUM Privacy_Violation /addOns/llm/src/main/java/org/zaproxy/addon/llm/services/LlmCommunicationService.java: 192
detailsMethod at line 192 of /addOns/llm/src/main/java/org/zaproxy/addon/llm/services/LlmCommunicationService.java sends user information outside the ap...
Attack Vector

Fixed Issues (3) Great job! The following issues were fixed in this Pull Request
Severity Issue Source File / Package
MEDIUM Privacy_Violation /addOns/llm/src/main/java/org/zaproxy/addon/llm/ui/LlmAppendHttpMessageMenu.java: 53
MEDIUM Privacy_Violation /addOns/llm/src/main/java/org/zaproxy/addon/llm/ui/LlmChatPanel.java: 240
MEDIUM Privacy_Violation /addOns/llm/src/main/java/org/zaproxy/addon/llm/services/LlmCommunicationService.java: 191

Use @Checkmarx to interact with Checkmarx PR Assistant.
Examples:
@Checkmarx how are you able to help me?
@Checkmarx rescan this PR

Signed-off-by: Simon Bennetts <psiinon@gmail.com>
@psiinon psiinon marked this pull request as ready for review March 5, 2026 13:09
@psiinon

psiinon commented Mar 5, 2026

Copy link
Copy Markdown
Member Author

Ready for review

@kingthorin kingthorin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't pulled the branch and tested, this is the only issue I see (very minor).

@kingthorin kingthorin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems good to me, just that one non-blocking date thing.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces multi-conversation support to the LLM add-on by converting the LLM Chat panel into a tabbed UI, and updates the add-on’s LLM communication listeners to surface request/response output in the chat UI.

Changes:

  • Refactors the LLM Chat panel into a tabbed interface with per-tab conversation panels, plus-tab creation, renaming, and close controls.
  • Reworks LLM communication listeners/handlers to write request/response events to the chat UI (GUI) or to logs (headless).
  • Updates i18n strings, help documentation, and the add-on changelog to reflect tabbed chat support.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
addOns/llm/src/main/resources/org/zaproxy/addon/llm/resources/Messages.properties Adds new i18n keys for tab rename and error labeling.
addOns/llm/src/main/javahelp/org/zaproxy/addon/llm/resources/help/contents/chat.html Documents new multi-tab chat behavior and context menu behavior per selected tab.
addOns/llm/src/main/java/org/zaproxy/addon/llm/ui/LlmNumberedRenamableTabbedPane.java New tabbed container implementation (plus tab, renaming, tagged tabs).
addOns/llm/src/main/java/org/zaproxy/addon/llm/ui/LlmCloseTabPanel.java New custom tab header with close button and title label.
addOns/llm/src/main/java/org/zaproxy/addon/llm/ui/LlmChatTabPanel.java New per-tab chat UI panel with its own input/output and conversation sending.
addOns/llm/src/main/java/org/zaproxy/addon/llm/ui/LlmChatPanel.java Refactors main chat panel into a container hosting the new tabbed UI.
addOns/llm/src/main/java/org/zaproxy/addon/llm/services/LlmLogResponseHandler.java New headless/log-only listener implementation.
addOns/llm/src/main/java/org/zaproxy/addon/llm/services/LlmGuiResponseHandler.java Refactors GUI listener to append LLM request/response events into chat tabs.
addOns/llm/src/main/java/org/zaproxy/addon/llm/services/LlmCommunicationService.java Updates service to accept a generic ChatModelListener and wires it into the model.
addOns/llm/src/main/java/org/zaproxy/addon/llm/ExtensionLlm.java Tracks the chat panel instance and wires GUI vs log listeners when creating comms services.
addOns/llm/CHANGELOG.md Updates add-on description to “tabbed LLM Chat panel.”
addOns/alertFilters/src/main/java/org/zaproxy/zap/extension/alertFilters/llm/LlmActionReviewAlert.java Removes call to switch focus to the Output tab (API removed).
Comments suppressed due to low confidence (2)

addOns/llm/src/main/java/org/zaproxy/addon/llm/services/LlmGuiResponseHandler.java:47

  • This listener updates Swing UI state (showTab(), setProcessing()) directly from the model callback thread. Swing components must only be accessed on the EDT; wrap these calls (and any direct UI state changes) in SwingUtilities.invokeLater/invokeAndWait. Also, chatPanel can be null (e.g. if tab lookup fails), which would cause an immediate NPE; add a null guard/fallback handler.
    addOns/llm/src/main/java/org/zaproxy/addon/llm/services/LlmCommunicationService.java:86
  • ChatModel is a static field but each LlmCommunicationService instance builds the model with an instance-specific listener (and provider config). With multiple concurrent comms services/tabs, the most recently constructed service will overwrite the static model (and its listeners), causing responses/events to be delivered to the wrong UI tab and mixing contexts. Make the ChatModel an instance field (or cache models by provider/config without embedding per-tab listeners) so each service can have an independent listener/context.
    private LlmAssistant llmAssistant;
    private ChatModelListener listener;
    @Getter private LlmProviderConfig pconf;
    @Getter private String modelName;
    private Requestor requestor;

    private static ChatModel model;
    private static ObjectMapper objectMapper = new ObjectMapper();
    private static ObjectWriter prettyWriter = objectMapper.writerWithDefaultPrettyPrinter();
    private ChatMemory chatMemory;

    public LlmCommunicationService(
            LlmProviderConfig pconf, String modelName, ChatModelListener listener) {
        this.pconf = pconf;
        this.modelName = modelName;
        this.listener = listener;
        chatMemory = MessageWindowChatMemory.withMaxMessages(10);
        model = buildModel();

        llmAssistant =
                AiServices.builder(LlmAssistant.class)
                        .chatModel(model)
                        .chatMemory(chatMemory)
                        .build();
        requestor = new Requestor(HttpSender.MANUAL_REQUEST_INITIATOR, new HistoryPersister());

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +53 to +93
addChangeListener(
new ChangeListener() {
private boolean adding = false;

@Override
public void stateChanged(ChangeEvent e) {
LlmNumberedRenamableTabbedPane ntp =
(LlmNumberedRenamableTabbedPane) e.getSource();
if (!adding && ntp.getSelectedIndex() == ntp.getTabCount() - 1) {
adding = true;
ntp.addDefaultTab();
adding = false;
}
}
});

addMouseListener(
new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent evt) {
if (evt.getClickCount() == 2) {
int index = indexAtLocation(evt.getX(), evt.getY());
if (index > -1 && index < getTabCount() - 1) {
Component comp = getTabComponentAt(index);
if (comp != null) {
String newName =
JOptionPane.showInputDialog(
Constant.messages.getString(
"llm.chat.tab.rename"),
comp.getName());
if (!StringUtils.isEmpty(newName)) {
comp.setName(newName);
}
}
}
}
}
});

addTab("", PLUS_ICON != null ? PLUS_ICON : null, hiddenComponent);
}
Comment on lines +99 to +118
public void addDefaultTab() {
addTab(nextTabName());
}

public LlmChatTabPanel addTab(String tabName) {
return addTab("CHAT-" + tabName, tabName);
}

public LlmChatTabPanel addTab(String tag, String tabName) {
int index = getTabCount() - 1;
LlmChatTabPanel pane = new LlmChatTabPanel(extension, tag);
insertTab(tabName, null, pane, null, index);
setTabComponentAt(index, new LlmCloseTabPanel(tabName, this, tag));
setSelectedIndex(index);
return pane;
}

public LlmChatTabPanel getTaggedTab(String tag, String tabName) {
return taggedTabs.computeIfAbsent(tag, k -> addTab(tag, tabName));
}
Comment on lines +46 to +73
public LlmCloseTabPanel(String tabName, LlmNumberedRenamableTabbedPane tabbedPane, String tag) {
super();
this.setOpaque(false);
lblTitle = new JLabel(tabName);
this.tag = tag;
JButton btnClose = new JButton();
btnClose.setOpaque(false);

btnClose.setRolloverIcon(CLOSE_TAB_RED_ICON);
btnClose.setRolloverEnabled(true);
btnClose.setContentAreaFilled(false);
btnClose.setToolTipText(Constant.messages.getString("all.button.close"));
btnClose.setIcon(CLOSE_TAB_GREY_ICON);
btnClose.setBorder(new EmptyBorder(0, 6, 0, 0));
btnClose.setBorderPainted(false);
btnClose.setFocusable(false);

GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 0;
gbc.weightx = 1;

this.add(lblTitle, gbc);

gbc.gridx++;
gbc.weightx = 0;
this.add(btnClose, gbc);

Comment on lines +111 to +120
public void actionPerformed(ActionEvent evt) {
JTabbedPane ntp = tabbedPane;

int index = ntp.indexOfTab(tabName);
if (index >= 0) {
if (ntp.getTabCount() > 2 && index == ntp.getTabCount() - 2) {
ntp.setSelectedIndex(index - 1);
}
ntp.removeTabAt(index);
}
Comment on lines 170 to +200
@@ -171,7 +194,10 @@ public LlmCommunicationService getCommunicationService(String commsKey, String o
new LlmCommunicationService(
options.getDefaultProviderConfig(),
options.getDefaultModelName(),
outputTabName));
this.hasView()
? new LlmGuiResponseHandler(
getChatTab(commsKey, outputTabName))
: new LlmLogResponseHandler()));
Comment on lines +204 to +235
appendMessage(
Constant.messages.getString(
"llm.chat.panel.message.format",
Constant.messages.getString(USER_LABEL),
message));

Thread chatThread =
new Thread(
() -> {
try {
LlmCommunicationService service =
extension.getCommunicationService(
tag,
Constant.messages.getString(
"llm.chat.output.panel"));
if (service == null) {
appendToOutput("llm.chat.panel.error.service", null);
return;
}
if (useStructuredPayload) {
ChatRequest chatRequest =
ChatRequest.builder()
.messages(
SystemMessage.from(
UNTRUSTED_DATA_SYSTEM_MESSAGE),
UserMessage.from(message))
.build();
ChatResponse response = service.chat(chatRequest);
appendToOutput(ASSISTANT_LABEL, response.aiMessage().text());
} else {
appendToOutput(ASSISTANT_LABEL, service.chat(message));
}
*
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
*
* Copyright 2025 The ZAP Development Team
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants